defmodule IocpMonitor.Worker.Receiver do
  @moduledoc """
  上行TCP包接收进程的定义模块。
  本模块负责接收电流盒子发送的上行数据，
  校验数据完整性和合法性，
  并调用路由模块（`IocpMonitor.Router`）分发收到的包。
  """

  require Logger
  use GenServer
  use Bitwise
  use IocpMonitor.Types
  import IocpMonitor.LittleEndian
  alias IocpMonitor.Router
  alias IocpMonitor.DeviceAddressHolders
  alias IocpMonitor.Worker.DeviceAddressHolder

  @packet_start [0x68, 0x16]
  @initial_timeout 300_000  # 5 minutes in milliseconds
  @timeout 5_000  # 5 seconds in milliseconds

  @spec start_link({tcp_client(), pid()}) :: :ok | {:error, term()}
  def start_link({_client, _sup} = arg) do
    GenServer.start_link(__MODULE__, arg)
  end

  @impl true
  def init({_client, _sup} = arg) do
    GenServer.cast(self(), :receive)
    {:ok, arg}
  end

  @impl true
  def handle_cast(:receive, {client, sup} = state) do
    device_address = get_device_address(sup)
    {action, [_, _ | payload]} = receive_one(client, device_address)
    Router.dispatch(action, device_address, payload)
    GenServer.cast(self(), :receive)
    {:noreply, state}
  end

  @impl true
  def terminate(reason, {client, sup}) do
    :gen_tcp.close(client)
    IocpMonitor.Client.Supervisor.terminate_child(sup)
  end

  @doc """
  接收设备地址
  """
  @spec receive_device_address(tcp_client()) :: device_address()
  def receive_device_address(client) do
    client
    |> receive_one()
    |> elem(1)
    |> to_i()
  end

  defp receive_one(client, device_address \\ nil) do
    {:ok, @packet_start ++ data_length_octets} = :gen_tcp.recv(client, 4, @initial_timeout)
    data_length = to_i(data_length_octets)
    {:ok, [_, action | payload] = data} = :gen_tcp.recv(client, data_length - 1, @timeout)
    {:ok, [checksum]} = :gen_tcp.recv(client, 1, @timeout)
    validate!(data, data_length_octets, checksum, action, device_address)
    {action, payload}
  end

  defp validate!([_, action, lo, hi | _] = data, data_length_octets, checksum, action, device_address) do
    ^checksum = (Enum.sum(data) + Enum.sum(data_length_octets) + Enum.sum(@packet_start)) &&& 0xFF
    if action != 0x04 do
      ^device_address = to_i([lo, hi])
    end
  end

  defp get_device_address(sup) do
    [{device_address_holder, _}] = Registry.lookup(DeviceAddressHolders, sup)
    DeviceAddressHolder.get(device_address_holder)
  end

  @doc false
  def child_spec(arg) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [arg]},
      type: :worker,
      restart: :temporary,
      shutdown: 500
    }
  end
end
